/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.messaging.core;

import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.SimpleMessageConverter;
import org.springframework.messaging.converter.SmartMessageConverter;
import org.springframework.util.Assert;

/**
 * Abstract base class for implementations of {@link MessageSendingOperations}.
 *
 * @author Mark Fisher
 * @author Rossen Stoyanchev
 * @author Stephane Nicoll
 * @since 4.0
 */
public abstract class AbstractMessageSendingTemplate<D> implements MessageSendingOperations<D> {

    /**
     * Name of the header that can be set to provide further information
     * (e.g. a {@code MethodParameter} instance) about the origin of the
     * payload, to be taken into account as a conversion hint.
     *
     * @since 4.2
     */
    public static final String CONVERSION_HINT_HEADER = "conversionHint";


    protected final Log logger = LogFactory.getLog(getClass());

    @Nullable
    private D defaultDestination;

    private MessageConverter converter = new SimpleMessageConverter();

    /**
     * Return the configured default destination.
     */
    @Nullable
    public D getDefaultDestination() {
        return this.defaultDestination;
    }

    /**
     * Configure the default destination to use in send methods that don't have
     * a destination argument. If a default destination is not configured, send methods
     * without a destination argument will raise an exception if invoked.
     */
    public void setDefaultDestination(@Nullable D defaultDestination) {
        this.defaultDestination = defaultDestination;
    }

    /**
     * Return the configured {@link MessageConverter}.
     */
    public MessageConverter getMessageConverter() {
        return this.converter;
    }

    /**
     * Set the {@link MessageConverter} to use in {@code convertAndSend} methods.
     * <p>By default, {@link SimpleMessageConverter} is used.
     *
     * @param messageConverter the message converter to use
     */
    public void setMessageConverter(MessageConverter messageConverter) {
        Assert.notNull(messageConverter, "MessageConverter must not be null");
        this.converter = messageConverter;
    }

    @Override
    public void send(Message<?> message) {
        send(getRequiredDefaultDestination(), message);
    }

    protected final D getRequiredDefaultDestination() {
        Assert.state(this.defaultDestination != null, "No 'defaultDestination' configured");
        return this.defaultDestination;
    }

    @Override
    public void send(D destination, Message<?> message) {
        doSend(destination, message);
    }

    protected abstract void doSend(D destination, Message<?> message);


    @Override
    public void convertAndSend(Object payload) throws MessagingException {
        convertAndSend(payload, null);
    }

    @Override
    public void convertAndSend(D destination, Object payload) throws MessagingException {
        convertAndSend(destination, payload, (Map<String, Object>) null);
    }

    @Override
    public void convertAndSend(D destination, Object payload, @Nullable Map<String, Object> headers)
            throws MessagingException {

        convertAndSend(destination, payload, headers, null);
    }

    @Override
    public void convertAndSend(Object payload, @Nullable MessagePostProcessor postProcessor)
            throws MessagingException {

        convertAndSend(getRequiredDefaultDestination(), payload, postProcessor);
    }

    @Override
    public void convertAndSend(D destination, Object payload, @Nullable MessagePostProcessor postProcessor)
            throws MessagingException {

        convertAndSend(destination, payload, null, postProcessor);
    }

    @Override
    public void convertAndSend(D destination, Object payload, @Nullable Map<String, Object> headers,
            @Nullable MessagePostProcessor postProcessor) throws MessagingException {

        Message<?> message = doConvert(payload, headers, postProcessor);
        send(destination, message);
    }

    /**
     * Convert the given Object to serialized form, possibly using a
     * {@link MessageConverter}, wrap it as a message with the given
     * headers and apply the given post processor.
     *
     * @param payload the Object to use as payload
     * @param headers headers for the message to send
     * @param postProcessor the post processor to apply to the message
     * @return the converted message
     */
    protected Message<?> doConvert(Object payload, @Nullable Map<String, Object> headers,
            @Nullable MessagePostProcessor postProcessor) {

        MessageHeaders messageHeaders = null;
        Object conversionHint = (headers != null ? headers.get(CONVERSION_HINT_HEADER) : null);

        Map<String, Object> headersToUse = processHeadersToSend(headers);
        if (headersToUse != null) {
            if (headersToUse instanceof MessageHeaders) {
                messageHeaders = (MessageHeaders) headersToUse;
            } else {
                messageHeaders = new MessageHeaders(headersToUse);
            }
        }

        MessageConverter converter = getMessageConverter();
        Message<?> message = (converter instanceof SmartMessageConverter ?
                              ((SmartMessageConverter) converter).toMessage(payload, messageHeaders, conversionHint) :
                              converter.toMessage(payload, messageHeaders));
        if (message == null) {
            String payloadType = payload.getClass().getName();
            Object contentType = (messageHeaders != null ? messageHeaders.get(MessageHeaders.CONTENT_TYPE) : null);
            throw new MessageConversionException("Unable to convert payload with type='" + payloadType +
                    "', contentType='" + contentType + "', converter=[" + getMessageConverter() + "]");
        }
        if (postProcessor != null) {
            message = postProcessor.postProcessMessage(message);
        }
        return message;
    }

    /**
     * Provides access to the map of input headers before a send operation.
     * Subclasses can modify the headers and then return the same or a different map.
     * <p>This default implementation in this class returns the input map.
     *
     * @param headers the headers to send (or {@code null} if none)
     * @return the actual headers to send (or {@code null} if none)
     */
    @Nullable
    protected Map<String, Object> processHeadersToSend(@Nullable Map<String, Object> headers) {
        return headers;
    }

}
